# -*- coding: utf-8 -*-
"""
:mod:`nubox.util`
=================

Provides utility functions used within ``nubox``.

"""
from __future__ import (division, with_statement, absolute_import, generators,
                         nested_scopes)
from math import floor

from .shim import (xrange, product, izip, bytes)
from .const import (TYPE_TO_TYPECODE, TYPECODES, ARRAY_TYPECODES, INT_TYPECODES,
                    FLOAT_TYPECODES, UNICODE_TYPECODES, OBJECT_TYPECODES)


def gettype(item):
    """
    Determines the ``type`` and typecode of an **item**. Returns a ``tuple`` 
    with a Python ``type`` or ``class`` and itemcode ``str``.
    
    Arguments:
    
      - item(``object``) an **item** to be placed in an ``NdArray``
    
    """
    item_type = type(item)
    try:
        item_typecode = TYPE_TO_TYPECODE[item_type]
    except KeyError:
        item_typecode = OBJECT_TYPECODES[0]
    return (item_type, item_typecode)

def compatypecodes(typecodes):
    """
    Determines if an iterable of typecodes is represents the same Python 
    ``type``. Returns a ``bool``. Typecodes that resolve to the same ``type``
    are "compatible" and generally can be stored in the same array without loss.
    
    Arguments:
    
      - typecodes(sequence) a sequence of typecodes from most likely from 
        ``nubox.const.TYPECODES``
    
    """
    all_tc = len(typecodes)
    for cls in (INT_TYPECODES, FLOAT_TYPECODES, UNICODE_TYPECODES,
                    OBJECT_TYPECODES):
        if len([tc for tc in typecodes if tc in cls]) == all_tc:
            return True
    return False

def checktypecode(typecode):
    """
    Determines ``bool`` values of typecode properties returns a ``tuple`` of 
    ``(istc, isatc, isutc, isotc, isftc, isitc)``, where:
    
      - ``istc`` is any valid typecode 
      - ``isatc`` is an ``array`` typecode (valid for ``array.array`` instances)
      - ``isutc`` is an ``unicode`` typecode
      - ``isotc`` is an ``object`` typecode
      - ``isftc`` is a ``float`` typecode
      - ``isitc`` is an ``int`` or ``long`` typecode
    
    Arguments:
    
      - typecode(``str``) A single character identifying the typecode most 
        likely one of ``nubox.const.TYPECODES``
      
    """
    istc = typecode in TYPECODES
    isatc = typecode in ARRAY_TYPECODES
    isutc = typecode in UNICODE_TYPECODES
    isotc = typecode in OBJECT_TYPECODES
    isftc = typecode in FLOAT_TYPECODES
    isitc = typecode in INT_TYPECODES
    return (istc, isatc, isutc, isotc, isftc, isitc)

def checkitem(item):
    """
    Determines **item** properties and returns ``bool`` values. Returns a 
    ``tuple`` of ``(isiterable, isarray, isarrayitem, islistitem)``, where:
    
      - ``isatomic`` is ``True`` if **item** is atomic i.e. not a container
      - ``isiterable`` is ``True`` if **item** is an iterable
      - ``isarray`` is ``True`` if **item** is an ``array.array`` instance
      - ``isarrayitem`` is ``True`` if **item** can be an ``array.array`` 
         **item**
      - ``islistitem`` is ``True`` if **item** can be a ``list`` **item**
      
    Arguments:
    
      - item(``object``) an **item** to be placed in an ``NdArray``
      
    """
    item_type, item_typecode = gettype(item)
    try:
        iter(item)
        isiterable = True
        if (item_type in (bytes, unicode)) and (len(item) == 1): # str or unicode
            isatomic = True
        else:
            isatomic = False
    except TypeError:
        isiterable = False
        isatomic = True

    isarray = hasattr(item_type, '_kwargs') and hasattr(item_type, 'data')

    if item_typecode in ARRAY_TYPECODES:
        isarrayitem = True
        islistitem = True
    elif item_typecode in TYPECODES:
        isarrayitem = False
        islistitem = True
    else:
        isarrayitem = False
        islistitem = False

    return (isatomic, isiterable, isarray, isarrayitem, islistitem)

def flatten(l):
    """
    Flattens a ``list`` or ``tuple``. Returns a ``list`` with all elements.
    
    Arguments:
    
      - l(sequence) A possibly nested ``list`` or ``tuple`` of elements
      
    """
    if isinstance(l, (list, tuple)):
        return sum(map(flatten, l), [])
    else:
        return [l]

def unnest(l):
    """
    For a given iterable of iterables returns a ``list``, which contains all 
    sub-sequences in a depth-first manner. For example given a nested iterable:
    ``[(1, 2, 3), (3, (4, 5))]`` it returns: ``[[(1, 2, 3), (3, (4, 5))], 
    (1, 2, 3), 1, 2, 3, (3, (4, 5)), 3, (4, 5), 4, 5]``.
     
    Arguments:
    
      - l(sequence) A possibly nested ``list`` or ``tuple`` of elements
      
    """
    u = []
    u.append(l)
    if isinstance(l, (list, tuple)):
        for e in l:
            u.extend(unnest(e))
    return u

def slices_shape2indices_shape(slices, shape):
    """
    Translates and iterable of ``slice`` instances and the corresponding "shape"
    i.e. lengths of each dimension into a ``tuple`` of indices and a new shape.
    This function de facto implements an nD-slice.
    
    Arguments:
    
      - slices(iterable of ``slice``)  a sequence of ``slice`` instances
      - shape(``tuple``) a sequence of dimensions i.e. lengths for each of the
        ``slices``.
        
    """
    if len(slices) != len(shape):
        raise ValueError("number of slices must match shape")
    nd_indices = []
    nd_shape = []
    for dim, slice in izip(shape, slices):
        sss = slice.indices(dim) # start,stop,step obeying len
        indices = range(*sss) # this -> [] if sss is an empty slice
        dim = len(indices)
        nd_indices.append(indices)
        nd_shape.append(dim)
    nd_indices = tuple(product(*nd_indices))
    nd_shape = tuple(nd_shape) if nd_indices else () # at least on 0 in nd_shape
    return (nd_indices, tuple(nd_shape))

def hex_to_rgb(value):
    """
    Converts a hex ``string`` into a RGB ``tuple``.   
    
    Arguments:
    
        - value(``str``) A ``string`` representation of a hexadecimal 
          number.
          
    """
    value = value.lstrip('#')
    lv = len(value)
    return tuple(int(value[i:i + int(lv / 3)], 16) for i in range(0, lv, int(lv / 3)))

def rgb_to_hex(rgb):
    """
    Converts a RGB ``sequence`` into a hex ``string``.
    
    Arguments:
        
        - rgb(sequence) A sequence of ``int`` in the 0-255 range.
        
    """
    return '#%02x%02x%02x' % rgb

def make_colors(breakpoints, length=240):
    """
    Creates a linear color map given RGB "breakpoints". Returns an array of 
    colors of specified "length". A breakpoint is any RGB color-tuple. By 
    default returns an array valid for ``hijack_colors``.
    
    Arguments:
    
      - breakpoints (sequence) A sequence of ``tuples`` of RGB values 
        each RGB value is a floating point number from ``0.`` to ``1.``.
      - length(``int``) [default: ``240``] The number of colors in the returned 
        ``list``
    """
    ln = len(breakpoints)
    rng = floor(length / (ln - 1)) # 
    if ln < 2:
        raise ValueError('at least 2 breakpoints required')
    breakids = []
    for i in xrange(0, ln):
        breakids.append(int(length * (i) / float(ln - 1)))
        # we have a dynamic range of 240

    colors = []
    for start in xrange(ln - 1):
        lor, log, lob = breakpoints[start]
        hir, hig, hib = breakpoints[start + 1]
        for id_ in xrange(*breakids[start:start + 2]):
            r = (id_ % (rng)) / float(rng) # linear space
            clr = [hir * r + lor * (1 - r),
                   hig * r + log * (1 - r),
                   hib * r + lob * (1 - r)]
            colors.append(clr)

    return colors
#
#def hijack_colors(colors):
#    """
#    Sets the 240 i.e. 256 - 16 available colors of a terminal to RGB values in
#    "colors". Colors should be a sequence of tuples or a rank-2 
#    ``numpy.ndarray``.
#    
#    Arguments:
#    
#        - colors (``sequence`` or ``numpy.ndarray``) A sequence of 240 RGB 
#          3-tuples.
#    """
#    if len(colors) != 240:
#        raise ValueError("Array has to be of length 240.")
#    init = ''
#    for i, (r, g, b) in enumerate(colors):
#        init += "\x1b]4;%d;rgb:%02x/%02x/%02x\x1b\\" % \
#                                (16 + i, int(r * 255), int(g * 255), int(b * 255))
#    init += '\n'
#    sys.stdout.write(init)
#
#def print_array(array, colors=None):
#    """
#    Prints an "array" to ``sys.stdout``. The color map is given by "colors". 
#    Custom color maps can be constructed by the ``make_colors`` function.
#    
#    Arguments:
#    
#      - array (``numpy.ndarray``) Any array which makes sense viewed as 
#        ``float``.
#      - colors (``sequence``) [default: ``COLDHOT``] See: ``make_colors`` and 
#        ``hijack_colors``
#        
#    """
#    if array.typecode not in FLOAT_TYPECODES:
#        raise ValueError('array must be of float typecode')
#    if len(array.shape) != 3:
#        raise ValueError('array must be of rank-3')
#    colors = colors or COLDHOT
#    hijack_colors(colors)
#    # shift to positive
#    min_ = array.item_min()[0]
#    max_ = array.item_max()[0]
#    # normalize array to 0-1
#    norm_array = array.new()
#    norm_array.map_item('sub', min_)
#    norm_array.map_item('div', max_)
#    data = []
#    for row in norm_array.child_iter_fast():
#        for el in row.child_iter_fast():
#            clr_idx = int(el.data[0] * 239) + 16
#            assert clr_idx < 256
#            data.append("\x1b[48;5;%dm  " % clr_idx)
#        data.append('\x1b[0m  \n')
#    sys.stdout.write("".join(data))

#def make_ruler(length, major=(20, '|'), minor=(10, '.')):
#    """
#    Constructs a ruler like that::
#    
#   #     |---------.---------|-------
#   #     0                   20   
#        
#    Allows to specify the distance and character of minor/major ticks. Returns
#    two strings for the ruler and for the numbers corresponding to major ticks.
#    
#    Arguments:
#    
#        - length (``int``) length of the constructed ruler
#        - major (``tuple``) [default: (10, '.')] A tuple of the form 
#          ``(N, 'X')``, where N is an integer, which means that a major-tick of 
#          character 'X' will be placed every N symbols starting and including 0.
#        - minor (``tuple``) [default: (10, '.')] A tuple of the form 
#          ``(M, 'Y')``, where M is an integer, which means that a minor-tick of 
#          character 'Y' will be placed every M symbols, but only where no 
#          major-tick is placed.
#    """
#    ruler = []
#    value = []
#    for pos in range(length):
#        if not pos % major[0]:
#            ruler.append('|')
#            s = "%" + ("-%ss" % major[0])
#            value.append(s % pos)
#        elif not pos % minor[0]:
#            ruler.append('.')
#        else:
#            ruler.append('-')
#    return ("".join(ruler), "".join(value))

#def print_strings(strings, raw_strings=(), ruler=True, width=80, **kwargs):
#    """
#    Formats and prints strings wrapped to the specified "width" and interleaved.
#    Optionally a ruler is included.
#    
#    Arguments:
#    
#        - strings (``sequence``) A sequence of strings.
#        - raw_strings (``sequence``) A sequence of pre-wrapped
#          strings. A wrapped string is a sequence of strings of right length.
#          This is useful if string includes non-printable characters.
#        - ruler (``bool``) If ``True`` a ruler will be included *below* the 
#          strings.
#        - width (``int``) The maximum width of the print-out.
#        
#    Additional keyworded arguments are passed to ``make_ruler``.
#    """
#    maxlen = max([len(string) for string in strings])
#    rul, num = make_ruler(maxlen, **kwargs)
#    wvals = []
#    for raw_seqs in raw_strings:
#        wvals.append(raw_seqs)
#    for seq in strings:
#        wvals.append(wrap(seq, width=width, break_on_hyphens=False))
#    if ruler:
#        wvals.append(wrap(rul, width=width, break_on_hyphens=False))
#        wvals.append(wrap(num, width=width, break_on_hyphens=False))
#
#    for block in izip_longest(*wvals):
#        for part in block:
#            if part:
#                sys.stdout.write(part)
#            sys.stdout.write('\n')
#        sys.stdout.write('\n')


GRAY = make_colors([
                    (0., 0., 0.),
                    (1., 1., 1.)
                    ])
COLDHOT = make_colors([
                       (0.0, 0.0, 1.0),
                       (0.0, 0.5, 1.0),
                       (1.0, 1.0, 1.0),
                       (1.0, 1.0, 0.0),
                       (1.0, 0.0, 0.0)
                       ])
COLD = make_colors([
                    (0.0, 0.0, 1.0),
                    (0.5, 0.5, 1.0),
                    (1.0, 1.0, 1.0)
                   ])
HOT = make_colors([
                   (1.0, 1.0, 1.0),
                   (1.0, 1.0, 0.0),
                   (1.0, 0.0, 0.0)
                   ])
HEAT = make_colors([
                    (0.0, 0.0, 0.0),
                    (1.0, 0.0, 0.0),
                    (1.0, 1.0, 0.0),
                    (1.0, 1.0, 1.0)
                    ])
